/*
 * Copyright (C) 2012 by Zhang Li <RichSelian at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include "nw-tcpserver.h"
#include "nw-exception.h"
#include "nw-allocator.h"
#include "nw-conninfo.h"

static int mainloop = 1;

static inline int setnonblocking(int fd) {
    int flags;
    if((flags = fcntl(fd, F_GETFL)) == -1) {
        return -1;
    }
    if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        return -1;
    }
    return 0;
}

static inline void sigintterm(int sig) {
    mainloop = 0;
    return;
}

int nw_tcp_server_setup(int port, int (*connection_handler)(int connsock, void* context), void* context) {
    int servsock = -1;
    int servpoll = -1;
    int connpoll = -1;
    struct sockaddr_in servaddr;
    struct epoll_event ev;
    struct epoll_event events[1024];
    int reuseaddr = 1;
    size_t maxevents = sizeof(events) / sizeof(ev);
    nw_conninfo_prioqueue_t* connqueue;
    nw_conninfo_node_t* connnode;

    if((connqueue = nw_conninfo_prioqueue_create(1048576)) == NULL) {
        fprintf(stderr, "nowhttpd-fatal: allocating memory failed.\n");
        return -1;
    }

    nw_try_block_start() {
        nw_throw_if(1, SIG_ERR == (signal(SIGPIPE, SIG_IGN)));
        nw_throw_if(1, SIG_ERR == (signal(SIGTERM, sigintterm)));
        nw_throw_if(1, SIG_ERR == (signal(SIGINT, sigintterm)));

        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(port);
        memset(servaddr.sin_zero, 0, sizeof(servaddr.sin_zero));

        nw_throw_if(1, -1 == (servsock = socket(AF_INET, SOCK_STREAM, 0)));
        nw_throw_if(1, -1 == setsockopt(servsock, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)));
        nw_throw_if(1, -1 == setnonblocking(servsock));
        nw_throw_if(1, -1 == bind(servsock, (struct sockaddr*)&servaddr, sizeof(servaddr)));
        nw_throw_if(1, -1 == listen(servsock, maxevents));


        nw_throw_if(1, -1 == (servpoll = epoll_create(256)));
        nw_throw_if(1, -1 == (connpoll = epoll_create(256)));

        memset(&ev, 0, sizeof(ev));
        ev.events = EPOLLIN;
        ev.data.fd = servsock;
        nw_throw_if(1, -1 == epoll_ctl(servpoll, EPOLL_CTL_ADD, servsock, &ev));

        while(mainloop) {
            int connsock = -1;
            struct sockaddr_in connaddr;
            int i;
            int n;
            socklen_t socklen = sizeof(connaddr);

            nw_try_block_start() {
                nw_throw_if(1, -1 == (n = epoll_wait(servpoll, events, maxevents, 0)));
                for(i = 0; i < n; i++) {
                    nw_throw_if(1, -1 == (connsock = accept(servsock, (struct sockaddr*)&connaddr, &socklen)));
                    nw_throw_if(1, -1 == setnonblocking(connsock));

                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.ptr = nw_conninfo_prioqueue_append(connqueue, connsock, &connaddr);
                    nw_throw_if(1, -1 == epoll_ctl(connpoll, EPOLL_CTL_ADD, connsock, &ev));
                    fprintf(stderr, "nowhttpd-log: connection from '%s'(fd = %d) accepted.\n",
                            inet_ntoa(connaddr.sin_addr),
                            connsock);
                }

                nw_throw_if(1, -1 == (n = epoll_wait(connpoll, events, maxevents, 1)));
                for(i = 0; i < n; i++) {
                    if(events[i].events & EPOLLIN) { /* serve a connection */
                        connnode = nw_conninfo_prioqueue_update(connqueue, (nw_conninfo_node_t*)ev.data.ptr);
                        fprintf(stderr, "nowhttpd-log: connection from '%s'(fd = %d) start serving...\n",
                                inet_ntoa(connnode->m_connaddr.sin_addr),
                                connnode->m_connsock);

                        if(NW_CONN_CLOSE == connection_handler(connnode->m_connsock, context)) {
                            connnode->m_handle_close = 0;
                            epoll_ctl(connpoll, EPOLL_CTL_DEL, connsock, &ev);
                            shutdown(connnode->m_connsock, SHUT_RD);
                            close(connnode->m_connsock);

                            fprintf(stderr, "nowhttpd-log: connection from '%s'(fd = %d) finished serving, closed.\n",
                                    inet_ntoa(connnode->m_connaddr.sin_addr),
                                    connnode->m_connsock);
                        } else {
                            connnode->m_handle_close = 1;
                            fprintf(stderr, "nowhttpd-log: connection from '%s'(fd = %d) finished serving, keepalive.\n",
                                    inet_ntoa(connnode->m_connaddr.sin_addr),
                                    connnode->m_connsock);
                        }
                    }
                }
                nw_conninfo_prioqueue_remove_timeout(connqueue, 10); /* clear dead connections */

            } nw_except(1) {
                fprintf(stderr, "nowhttpd-log: a bad connection refused.\n");
                shutdown(connsock, SHUT_RD);
                close(connsock);

            } nw_try_block_end();
        }

    } nw_except(1) {
        fprintf(stderr, "nowhttpd-fatal: %s.\n", strerror(errno));
        close(servpoll);
        close(connpoll);
        close(servsock);
        nw_conninfo_prioqueue_destroy(connqueue);
        return -1;

    } nw_try_block_end();

    fprintf(stderr, "nowhttpd-log: nowhttpd exiting...\n");
    close(servpoll);
    close(connpoll);
    close(servsock);
    nw_conninfo_prioqueue_destroy(connqueue);
    return 0;
}
